/***************************************************************
*  Copyright (c) 2008 by GroupMe! Team (www.groupme.net)
*  All rights reserved.
*
*  This file is part of the GroupMe! Project. Source code of 
*  this project is closed and redistribution of this code is
*  prohibited. 
*  
*  Contact: http://www.groupme.net
*
*  This copyright notice MUST APPEAR in all copies of the file!
***************************************************************/
package net.groupme.search.strategy.social;

import java.sql.ResultSet;
import java.sql.SQLException;
import java.sql.Types;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;

import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.object.SqlUpdate;
import org.springframework.jdbc.support.GeneratedKeyHolder;

import net.groupme.model.ObjectFactory;
import net.groupme.model.Tag;
import net.groupme.search.strategy.model.WeightedIdentifiableComparatorByWeight;
import net.groupme.search.strategy.model.WeightedTag;
import net.groupme.utils.sql.SelectQueryUtility;

import cern.colt.matrix.DoubleMatrix2D;
import cern.colt.matrix.impl.SparseDoubleMatrix2D;

/**
 * created on May 26, 2008
 * 
 * This class provides static methods to compute the SocialSimRank, which 
 * is a ranking score describing the similarity between two tags.
 * 
 * @author Fabian Abel, <a href="mailto:abel@l3s.de">abel@l3s.de</a>
 * @author last edited by: $Author: fabian $
 * 
 * @version $Revision: 1.4 $ $Date: 2008-08-22 21:50:04 $
 */
public class SocialSimRank {
	
	/**
	 * The {@link SSRPreference} tell us how to construct the tag-resource-Matrix, which builds the base in order to compute the SocialSimRank. 
	 */
	public enum SSRPreference {
		/** Only traditional tag assignments, groups are treated as normal resources */
		TRADITIONAL_TAG_ASSIGNMENTS(1, "Select tagId, resourceId, count(*) as users from userTagGroupResource group by resourceId, tagId order by resourceId, tagId desc")
		
		;
			
			/** the preference strategy indicator */
			private int strategy = -1;
			private String tasQuery = null;
			private SSRPreference(int strategy, String tasQuery){
				this.strategy = strategy;
				this.tasQuery = tasQuery;
			}
			/** returns the strategy indicator */
			public int getStrategyIndicator(){
				return this.strategy;
			}
			/** return the query that returns the tag-resource-scores */
			public String getTasQuery(){
				return this.tasQuery;
			}


	}
	

	/** Matrix M_AP: M_AP(a_x, p_y) tells us how many users have assigned tag a_x to resource p_y */
	private static DoubleMatrix2D tagResourceMatrix = null;
	
	/** SocialSimRank Matrix, which contains the similarity between tags */
	private static DoubleMatrix2D ssrMatrix = null;
	
	/** all tags (in-memory) */
	private static List<Tag> tags = null;
	
	/** C_A and C_P from the SSR algorithm */
	public static final double SSR_DAMPING_FACTOR = 0.7;
	
	public static double convergenceCriteria = 0.005;
	public static int maxIterations = 10;
	
	public static void init(SSRPreference pref){
		initTagResourceMatrix(pref);
		computeSocialSimRankMatrix(pref);
		initTags();
	}
	
	/**
	 * Returns the SSR for a given tag. Tags that have a SSR of 0.0 are not included within the ranking.
	 * @param keyword the tag
	 * @param topK number of top k tags of the SSR
	 * @param runiID the ID of the run (evaluationSSR.strategy), in which the SSR ranking was computed
	 * @return the SSR for a given tag. Tags that have a SSR of 0.0 are not included within the ranking.
	 */
	public static List<WeightedTag> getSocialSimRankForTagFromDB(String keyword, int topK, int runiID){
		Tag dbTag = ObjectFactory.getTag(keyword);

		String query = "SELECT b.id, b.keyword, s.ssr FROM evaluationSSR as s, Tag as a, Tag as b where s.strategy = " + runiID + " AND a.id = " + dbTag.getId() + " AND a.id = s.tagi and b.id = tagj AND b.keyword != \"" + keyword + "\" order by ssr desc limit " + topK;
		ResultSet ssrValues = SelectQueryUtility.executeQuery(query);
		List<WeightedTag> ranking = new ArrayList<WeightedTag>();
		try {
			while(ssrValues.next()){
				ranking.add(new WeightedTag(ObjectFactory.getInMemoryTag(ssrValues.getString(1), ssrValues.getString(2), null), ssrValues.getDouble(3)));
			}
			
			query = "SELECT b.id, b.keyword, s.ssr FROM evaluationSSR as s, Tag as a, Tag as b where s.strategy = " + runiID + " AND a.id = " + dbTag.getId() + " AND a.id = s.tagj and b.id = tagi AND b.keyword != \"" + keyword + "\" order by ssr desc limit " + topK;

			ssrValues = SelectQueryUtility.executeQuery(query);
			WeightedTag wt = null;
			while(ssrValues.next()){
				wt = new WeightedTag(ObjectFactory.getInMemoryTag(ssrValues.getString(1), ssrValues.getString(2), null), ssrValues.getDouble(3));
				if(ranking.contains(wt)){
					if(ranking.get(ranking.indexOf(wt)).getWeight() < wt.getWeight()){
						ranking.remove(wt);
						ranking.add(wt);
					}
				}else{
					ranking.add(wt);
				}
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
		Collections.sort(ranking, new WeightedIdentifiableComparatorByWeight());
		if(ranking.size() < topK){
			return ranking;
		}
		return ranking.subList(0, topK);
	}
	
	public static List<WeightedTag> getSocialSimRankForTag(String keyword, int topK){
		if(ssrMatrix == null){
			init(SSRPreference.TRADITIONAL_TAG_ASSIGNMENTS);
		}
		
		Tag dbTag = ObjectFactory.getTag(keyword);
		int tagIndex = Integer.parseInt(dbTag.getId()) - 1;
		
		List<WeightedTag> ranking = new ArrayList<WeightedTag>();
		for(Tag t: tags){
			ranking.add(new WeightedTag(t, ssrMatrix.get(tagIndex, Integer.parseInt(t.getId()) - 1)));
		}
		Collections.sort(ranking, new WeightedIdentifiableComparatorByWeight());
		if(ranking.size() < topK){
			return ranking;
		}
		return ranking.subList(0, topK);
	}
	
	
	private static void initTags(){
		ResultSet tagSet = SelectQueryUtility.executeQuery("SELECT id, keyword FROM Tag");
		tags = new ArrayList<Tag>();
		try {
			while(tagSet.next()){
				tags.add(ObjectFactory.getInMemoryTag(tagSet.getString(1), tagSet.getString(2), null));
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
	
	/**
	 * Initialize the tag-resource Matrix M_AP (M_AP(a_x, p_y) tells us how many users have assigned 
	 * tag a_x to resource p_y)
	 * @param pref how to construct the tag-resource-Matrix, which builds the base in order to compute the SocialSimRank
	 */
	private static void initTagResourceMatrix(SSRPreference pref){		
		//damit wir nicht zwischen Matrix-Indizes und IDs mappen muessen entspricht eine ID auch dem Index in der Matrix
		//Achtung: Zu manchen Indizes existieren keine IDs! 
		ResultSet matrixSize = null;
		try {
			int tags = -1, resources = -1;
			matrixSize = SelectQueryUtility.executeQuery("SELECT max(id) FROM Tag");
			if(matrixSize.next()){
				tags = matrixSize.getInt(1);
			}
			matrixSize = SelectQueryUtility.executeQuery("SELECT max(id) FROM Resource");
			if(matrixSize.next()){
				resources = matrixSize.getInt(1);
			}
			tagResourceMatrix = new SparseDoubleMatrix2D(tags, resources);
			
			
			//Matrix konstruieren:
			ResultSet tagResourceScore = SelectQueryUtility.executeQuery(pref.getTasQuery());
			while(tagResourceScore.next()){
				tagResourceMatrix.set(tagResourceScore.getInt(1) - 1, // tag ID
						tagResourceScore.getInt(2) - 1, // resource ID 
						tagResourceScore.getDouble(3)); //score
			}
		} catch (SQLException e) {
			e.printStackTrace();
		}
	}
	
	/** 
	 * Computes the similarity between tags. This is the main method, which actually computes the SocialSimRank!
	 * @return similarity matrix
	 */
	private static void computeSocialSimRankMatrix(SSRPreference pref){
		if(tagResourceMatrix == null){
			initTagResourceMatrix(pref);
		}
		//init similarity matrix with S_A(ai,ai) = 1
		DoubleMatrix2D simBetweenTags = new SparseDoubleMatrix2D(tagResourceMatrix.rows(), tagResourceMatrix.rows());
		for(int i=0; i<simBetweenTags.columns(); i++){
			simBetweenTags.set(i, i, 1.0);
		}
		//init similarity matrix with S_P(pi,pi) = 1
		DoubleMatrix2D simBetweenResources = new SparseDoubleMatrix2D(tagResourceMatrix.columns(), tagResourceMatrix.columns());
		for(int i=0; i<simBetweenResources.columns(); i++){
			simBetweenResources.set(i, i, 1.0);
		}
		
		double delta = 1000.0; int iterationK = 0;
		double sim_k_otherTag = 0.0;
		double factorS_A = 0.0;
		double numberOfResourcesAnnotatedWithTag = 0.0;
		List<Integer> resourcesAnnotatedWithTag = new ArrayList<Integer>();
		double numberOfResourcesAnnotatedWithOtherTag = 0.0;
		List<Integer> resourcesAnnotatedWithOtherTag = new ArrayList<Integer>();
		
		//sim resources:
		double sim_k_otherResources = 0.0; double factorS_P = 0.0;
		List<Integer> annotationsOfResM = new ArrayList<Integer>();
		List<Integer> annotationsOfResN = new ArrayList<Integer>();
		
		for(int tagIndex = 0; tagIndex < simBetweenTags.columns(); tagIndex++){
			numberOfResourcesAnnotatedWithTag = getNumberOfResourcesAnnotatedWithTagIndex(tagIndex);
			resourcesAnnotatedWithTag = getResourcesAnnotatedWithTagIndex(tagIndex);
			
			for(int otherTag = 0; otherTag < simBetweenTags.columns(); otherTag++){
				numberOfResourcesAnnotatedWithOtherTag = getNumberOfResourcesAnnotatedWithTagIndex(otherTag);
				resourcesAnnotatedWithOtherTag = getResourcesAnnotatedWithTagIndex(otherTag);
				
				if( otherTag > tagIndex && //performance
					otherTag != tagIndex && //die Aehnlichkeit zu sich selbst ist 1 und braucht nicht extra berechnet werden
						numberOfResourcesAnnotatedWithOtherTag > 0.0 && numberOfResourcesAnnotatedWithTag > 0.0){ 
					iterationK = 0; delta = 1000.0; 
					factorS_A = SSR_DAMPING_FACTOR / (numberOfResourcesAnnotatedWithTag * numberOfResourcesAnnotatedWithOtherTag); 
						
					while(delta > convergenceCriteria && iterationK < maxIterations){
						
						//Similarity between tags: S_A(tagIndex, otherTag)
						sim_k_otherTag = 0.0;
						for(Integer resM: resourcesAnnotatedWithTag){
							for(Integer resN: resourcesAnnotatedWithOtherTag){
								sim_k_otherTag += 
									(
											(tagResourceMatrix.get(tagIndex, resM) < tagResourceMatrix.get(otherTag, resN) ? tagResourceMatrix.get(tagIndex, resM) : tagResourceMatrix.get(otherTag, resN)) 
									 	/
									 	(tagResourceMatrix.get(tagIndex, resM) > tagResourceMatrix.get(otherTag, resN) ? tagResourceMatrix.get(tagIndex, resM) : tagResourceMatrix.get(otherTag, resN)) 
									) 
									*
									simBetweenResources.get(resM, resN);
								
							}
						}
						sim_k_otherTag = factorS_A * sim_k_otherTag;
						delta = Math.abs(simBetweenTags.get(tagIndex, otherTag) - sim_k_otherTag);
						simBetweenTags.set(tagIndex, otherTag, sim_k_otherTag);
						simBetweenTags.set(otherTag, tagIndex, sim_k_otherTag);
						//if(sim_k_otherTag > 0)
						//System.out.println("sim(" + tagIndex + ", " + otherTag + ") = " + sim_k_otherTag);
						
						
						//Similarity between resources: S_P(page_i, page_j)
						for(Integer resM: resourcesAnnotatedWithTag){
							annotationsOfResM = getAnnotationsForResource(resM);
							for(Integer resN: resourcesAnnotatedWithOtherTag){
								annotationsOfResN = getAnnotationsForResource(resN);
								if( resN > resM &&
									annotationsOfResM.size() > 0 && annotationsOfResN.size() > 0 &&
										resN != resM){
									factorS_P = SSR_DAMPING_FACTOR / ( annotationsOfResM.size() * annotationsOfResN.size() * 1.0);
									sim_k_otherResources = 0.0;
									
									for(Integer tagi: annotationsOfResM){
										for(Integer tagj: annotationsOfResN){
											sim_k_otherResources += 
												(
														(tagResourceMatrix.get(tagi, resM) < tagResourceMatrix.get(tagj, resN) ? tagResourceMatrix.get(tagi, resM) : tagResourceMatrix.get(tagj, resN)) 
												 	/
												 	(tagResourceMatrix.get(tagi, resM) > tagResourceMatrix.get(tagj, resN) ? tagResourceMatrix.get(tagi, resM) : tagResourceMatrix.get(tagj, resN)) 
												) 
												*
												simBetweenTags.get(tagi, tagj);
										}
									}
									sim_k_otherResources = factorS_P * sim_k_otherResources;
									simBetweenResources.set(resM, resN, sim_k_otherResources);
									simBetweenResources.set(resN, resM, sim_k_otherResources);
								}
								
							}
						}//end: sim between resources
						
						iterationK++;
						//System.out.println("Iteration " + iterationK);
					}//end of "Until-Convergence-loop"
					if(simBetweenTags.get(tagIndex, otherTag) != 0.0){
						addSSR(tagIndex + 1, otherTag + 1, simBetweenTags.get(tagIndex, otherTag) , run);
				
					}
				}
			}
			System.out.println("Tag: " + tagIndex);
		}
		ssrMatrix =  simBetweenTags;
	}
	
	private static double getNumberOfResourcesAnnotatedWithTagIndex(int tagIndex){
		double numberResourcesAnnotatedWithTagIndex = 0.0;
		for(int resId = 0; resId < tagResourceMatrix.columns(); resId++){
			if(tagResourceMatrix.get(tagIndex, resId) > 0){
				numberResourcesAnnotatedWithTagIndex++;
			}
		}
		return numberResourcesAnnotatedWithTagIndex;
	}
	
	private static List<Integer> getResourcesAnnotatedWithTagIndex(int tagIndex){
		List<Integer> resources = new ArrayList<Integer>();
		for(int resId = 0; resId < tagResourceMatrix.columns(); resId++){
			if(tagResourceMatrix.get(tagIndex, resId) > 0){
				resources.add(resId);
			}
		}
		return resources;
	}
	
	private static List<Integer> getAnnotationsForResource(int resIndex){
		List<Integer> tags = new ArrayList<Integer>();
		for(int tag = 0; tag < tagResourceMatrix.rows(); tag++){
			if(tagResourceMatrix.get(tag, resIndex) > 0){
				tags.add(tag);
			}
		}
		return tags;
	}
	

	/**
	 * Inserts a EvaluationRecommedationStrategy-Run into the DB and returns the ID of the strategy.
	 * @param name name of the strategy
	 * @param description description of the strategy(-run)
	 * @return the ID of the strategy(-run) or <code>null</code> if something went wrong
	 */
	private static final void addSSR(Integer tagi, Integer tagj, Double ssr, String run){
		SqlUpdate su = new SqlUpdate(ObjectFactory.ds, "INSERT INTO evaluationSSR "  //$NON-NLS-1$
				+ " (tagi, tagj, ssr, strategy) VALUES (?, ?, ?, ?)");
		su.declareParameter(new SqlParameter("tagi", Types.INTEGER)); //$NON-NLS-1$
		su.declareParameter(new SqlParameter("tagj", Types.INTEGER)); //$NON-NLS-1$
		su.declareParameter(new SqlParameter("ssr", Types.DOUBLE)); //$NON-NLS-1$
		su.declareParameter(new SqlParameter("strategy", Types.INTEGER)); //$NON-NLS-1$
		su.compile();
		Object[] oa = {tagi, tagj, ssr, run};
		GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
		int count = su.update(oa, keyHolder);
		if (count == 0)
			System.out.println("problem");
	}
	
	/**
	 * Inserts a EvaluationRecommedationStrategy-Run into the DB and returns the ID of the strategy.
	 * @param name name of the strategy
	 * @param description description of the strategy(-run)
	 * @return the ID of the strategy(-run) or <code>null</code> if something went wrong
	 */
	private static final String addEvaluationStrategyRun(String name, String description){
		SqlUpdate su = new SqlUpdate(ObjectFactory.ds, "INSERT INTO EvaluationRecommedationStrategy "  //$NON-NLS-1$
				+ " (name, description) VALUES (?, ?)");
		su.declareParameter(new SqlParameter("name", Types.VARCHAR)); //$NON-NLS-1$
		su.declareParameter(new SqlParameter("description", Types.VARCHAR)); //$NON-NLS-1$
		su.compile();
		Object[] oa = new Object[2];
		oa[0] = new String(name);
		oa[1] = new String(description);
		GeneratedKeyHolder keyHolder = new GeneratedKeyHolder();
		int count = su.update(oa, keyHolder);
		if (count == 0)
			return null;
		return keyHolder.getKey().toString();
	}
	
	
	
	
	private static String run = null;
	
	public static int SSR_ID_IN_DATABASE = 1;
	
	/** Dummy testing  and stuff */
	public static void main(String[] args) {
//		run = addEvaluationStrategyRun("convergenceCriteria: " + convergenceCriteria + " (no group context)", "Social Sim Rank");
//		System.out.println(run);
//		
//		List<WeightedTag>  ranking = getSocialSimRankForTag("mensa", 20);//, SSR_ID_IN_DATABASE);
//		for(WeightedTag wt: ranking){
//			System.out.println(wt.getWeight() + "  -  " + wt.getIdentifiable().getLabel());
//		}
	}
}
